# coding=utf-8
import datetime
import httprunner
from flask import current_app
from httprunner.api import HttpRunner
from httprunner.loader import load
from httprunner import loader, exceptions, report, utils, parser
from loguru import logger
from markupsafe import Markup
from sentry_sdk import capture_message
from flask.json import JSONEncoder
from apps.interface.util.global_variable import *
from apps.interface.util.utils import *
from apps.interface.util.super_run import SuperRun
from apps.interface.util.aladdin_parse import _parse_testcase
from apps.interface.business.interfacereport import InterfaceReportBusiness
from apps.interface.business.interfaceproject import InterfaceProjectBusiness
from apps.interface.business.interfacecaserelation import InterfaceCaseRelationBusiness
from apps.interface.business.caseconfig import ConfigBusiness
from apps.interface.models.interfaceapimsg import InterfaceApiMsg
from apps.interface.models.interfacecase import InterfaceCase
from apps.interface.models.interfacecasedata import InterfaceCaseData
from apps.interface.models.interfaceconfig import InterfaceConfig
from apps.interface.models.interfaceproject import InterfaceProject
from apps.interface.util.super_run import get_type_dict
from library.api.db import db


class AladdinHttpRunner(HttpRunner):
    """
    修改HttpRunner，用例初始化时导入函数
    """
    
    def __init__(self):
        super(AladdinHttpRunner, self).__init__()
    
    def run_tests(self, tests_mapping):
        """ run testcase/testsuite data
        """
        capture_message("start to run tests")
        self.test_path = tests_mapping.get("x", {}).get("test_path", "")
        
        if self.save_tests:
            utils.dump_json_file(
                tests_mapping,
                utils.prepare_log_file_abs_path(self.test_path, "loaded.json")
            )
        
        # parse tests
        self.exception_stage = "parse tests"
        parsed_testcases = parser.parse_tests(tests_mapping)
        parse_failed_testfiles = parser.get_parse_failed_testfiles()
        if parse_failed_testfiles:
            logger.warning("parse failures occurred ...")
            utils.dump_json_file(
                parse_failed_testfiles,
                utils.prepare_log_file_abs_path(self.test_path, "parse_failed.json")
            )
        
        # parse error
        if len(parsed_testcases) != len(tests_mapping['testcases']):
            need_testcases = copy.deepcopy(tests_mapping['testcases'])
            for index, need_case in enumerate(need_testcases):
                for parse_case in parsed_testcases:
                    if parse_case['config']['name'] == need_case['config']['name']:
                        # 失败用例过滤重复->用例执行多次的情况
                        break
                else:
                    logger.error("failed to parse test case,{}".format(need_case['config']['name']))
                    parse_data = _parse_testcase(need_case,
                                                 tests_mapping.get("project_mapping", {}),
                                                 case_name=need_case['config']['name'])
                    del need_case['name']
                    del need_case['type']
                    parsed_testcases.insert(index, parse_data)
        
        if self.save_tests:
            utils.dump_json_file(
                parsed_testcases,
                utils.prepare_log_file_abs_path(self.test_path, "parsed.json")
            )
        
        # add tests to test suite
        self.exception_stage = "add tests to test suite"
        test_suite = self._add_tests(parsed_testcases)
        
        # run test suite
        self.exception_stage = "run test suite"
        results = self._run_suite(test_suite)
        
        # aggregate results
        self.exception_stage = "aggregate results"
        self._summary = self._aggregate(results)
        
        # generate html report
        self.exception_stage = "generate html report"
        report.stringify_summary(self._summary)
        
        if self.save_tests:
            utils.dump_json_file(
                self._summary,
                utils.prepare_log_file_abs_path(self.test_path, "summary.json")
            )
            # save variables and export data
            vars_out = self.get_vars_out()
            utils.dump_json_file(
                vars_out,
                utils.prepare_log_file_abs_path(self.test_path, "io.json")
            )
        
        return self._summary
    
    def run(self, path_or_tests, dot_env_path=None, mapping=None):
        """ main interface.

        Args:
            path_or_tests:
                str: testcase/testsuite file/foler path
                dict: valid testcase/testsuite data
            dot_env_path (str): specified .env file path.
            mapping (dict): if mapping is specified, it will override variables in config block.

        Returns:
            dict: result summary

        """
        current_app.logger.info('httprun:start')
        aladdin_case = {
            "project_mapping": {
                "functions": {},
                "variables": {},  # optional, priority 1
                "env": {}
            },
            "testcases": path_or_tests
        }
        for test_item in path_or_tests:
            config_functions = test_item.get('config')['import_module_functions']
            for f in config_functions:
                imported_module = importlib.reload(importlib.import_module(f))
                debugtalk_module = load.load_module_functions(imported_module)
                aladdin_case["project_mapping"]["functions"].update(debugtalk_module)
        
        logger.info(f"HttpRunner version: {httprunner.__version__}")
        if loader.is_test_path(aladdin_case):
            return self.run_path(aladdin_case, dot_env_path, mapping)
        elif loader.is_test_content(aladdin_case):
            project_working_directory = aladdin_case.get("project_mapping", {}).get("PWD",
                                                                                    os.getcwd())
            loader.init_pwd(project_working_directory)
            return self.run_tests(aladdin_case)
        else:
            raise exceptions.ParamsError(f"Invalid testcase path or testcases: {aladdin_case}")


# http_runner
def main_ate(cases):
    runner = AladdinHttpRunner()
    summary = runner.run(cases)
    return summary


class RunCase(object):
    def __init__(self, project_ids=None, env=None):
        self.project_ids = project_ids
        self.pro_config_data = None
        self.pro_base_url = None
        self.new_report_id = None
        self.env = env
        self.init_project_data(env)
    
    def init_project_data(self, env):
        interface_project_data = InterfaceProject.query.filter_by(id=self.project_ids,
                                                                  status=InterfaceProject.ACTIVE).first()
        self.pro_config_data = self.pro_config(interface_project_data)
        if interface_project_data:
            # 优先使用指定环境变量
            if env:
                base_environmental = env
            else:
                base_environmental = interface_project_data.environment_choice
        else:
            raise ValueError("查询用例环境出错")
        pro_base_url = {}
        for pro_data in InterfaceProject.query.filter_by(status=0).all():
            if base_environmental == 'first':
                pro_base_url['{}'.format(pro_data.id)] = json.loads(pro_data.host_test)
            elif base_environmental == 'second':
                pro_base_url['{}'.format(pro_data.id)] = json.loads(pro_data.host_dev)
            if base_environmental == 'third':
                pro_base_url['{}'.format(pro_data.id)] = json.loads(pro_data.host_online)
            if base_environmental == 'fourth':
                pro_base_url['{}'.format(pro_data.id)] = json.loads(pro_data.host_backup)
        self.pro_base_url = pro_base_url
    
    @staticmethod
    def pro_config(project_data):
        """
        把project的配置数据解析出来
        :param project_data:
        :return:
        """
        pro_cfg_data = {
            'config': {'name': 'config_name', 'request': {}, 'output': []},
            'teststeps': [],
            'name': 'config_name'
        }
        
        pro_cfg_data['config']['request']['headers'] = {h['key']: h['value'] for h in
                                                        json.loads(project_data.headers) if
                                                        h.get('key')}
        pro_cfg_data['config']['variables'] = json.loads(project_data.variables)
        return pro_cfg_data
    
    @staticmethod
    def assemble_step(api_id=None, step_data=None, global_headers=None, pro_base_url=None,
                      status=False, num=None):
        """
        :param api_id:
        :param step_data:
        :param global_headers:
        :param pro_base_url:
        :param status: 判断是接口调试(false)or业务用例执行(true)
        :param num: 第几个步骤
        :return:
        """
        if status:
            # 获取api基础信息；case只包含可改变部分所以还需要api基础信息组合成全新的用例
            api_data = InterfaceApiMsg.query.filter_by(id=step_data.api_msg_id,
                                                       status=InterfaceApiMsg.ACTIVE).first()
            current_app.logger.info('api_data:'.format(api_data))
            isTrans = json.loads(step_data.status_header)
            if isTrans[1]:
                api_data.header = step_data.header
        else:
            # 基础信息和参数信息都在api里面，所以api_case = case_data，直接赋值覆盖
            api_data = InterfaceApiMsg.query.filter_by(id=api_id,
                                                       status=InterfaceApiMsg.ACTIVE).first()
            step_data = api_data
        _data = {
            'name': step_data.name,
            'request': {
                'method': api_data.method,
                'files': {},
                'data': {}
            }
        }
        if status:
            _data['times'] = step_data.time
        # 获取公共变量
        SuperRun(_data, api_data, step_data, global_headers, status)
        # 获取私有变量
        if api_data.api_type not in get_type_dict():
            raise Exception('接口类型不存在')
        _data = get_type_dict().get(api_data.api_type).get_data(_data, api_data, pro_base_url)
        current_app.logger.info('第{}个步骤,_data的数据: {}'.format(num, _data))
        return _data
    
    def get_api_test(self, api_ids, config_id):
        temp_case = []
        case_env = self.get_environment()
        config_data = InterfaceConfig.query.filter_by(id=config_id,
                                                      status=InterfaceConfig.ACTIVE).first()
        if config_data is None:
            raise Exception('请选择配置')
        func_json_value = json.loads(config_data.func_address)
        func_name_list = []
        func_method_dict = {}
        if func_json_value:
            for value in func_json_value:
                if value not in func_name_list:
                    func_name_list.append(value)
                    import_path = 'func_list.{}'.format(value.replace('.py', ''))
                    func_list = importlib.import_module(import_path)
                    importlib.reload(func_list)
                    module_functions_dict = {name: item for name, item in vars(func_list).items() if
                                             isinstance(item, types.FunctionType)}
                    func_method_dict.update(module_functions_dict)
        # 替换全局变量值xxx@global，替换出错了设置为空字符
        self.set_global_variable('global', self.pro_config_data['config']['variables'],
                                 func_method_dict)
        for api_id in api_ids:
            api = InterfaceApiMsg.query.filter_by(id=api_id, status=0).first()
            
            case_list = [{'apiMsgId': api.id,
                          'extract': api.extract,
                          'case_name': api.name,
                          'num': api.num,
                          'header': api.header,
                          'json_variable': api.json_variable,
                          'param': api.param,
                          'validate': api.validate,
                          'variable': api.variable}] if api else None
            
            variable_check = check_case(case_list, json.dumps(func_json_value))
            if variable_check:
                raise Exception(variable_check)
        
        _temp_config = copy.deepcopy(self.pro_config_data)
        _config = json.loads(config_data.variables) if config_id else []
        if config_id:
            _temp_config['config']['import_module_functions'] = ['func_list.{}'.format(
                f.replace('.py', '')) for f in json.loads(config_data.func_address)]
        else:
            _temp_config['config']['import_module_functions'] = []
        # 接口忽略全局局部变量开关
        _temp_config = merge_config(_temp_config, _config)
        _temp_config['teststeps'] = [
            self.assemble_step(api_id=api_id,
                               global_headers=_temp_config['config']['request']['headers'],
                               pro_base_url=self.pro_base_url) for api_id in api_ids]
        # 处理variables值List转Dict
        variables_dict = {}
        config_variables = copy.deepcopy(_temp_config['config']['variables'])
        for item in config_variables:
            for key in item.keys():
                variables_dict[key] = item[key]
        # 处理env标签变量
        self.handler_env_variable(case_env, variables_dict)
        _temp_config['config']['variables'] = variables_dict
        del _temp_config['config']['request']
        temp_case.append(_temp_config)
        return temp_case
    
    def get_case_test(self, case_ids, case_type=None, project_id=None):
        case_env = self.get_environment()
        # 所有case_id引用的py文件的方法
        func_method_dict = self.get_func_methods(case_ids)
        # 替换全局变量值xxx@global，替换出错了设置为空字符
        self.set_global_variable('global', self.pro_config_data['config']['variables'],
                                 func_method_dict)
        # 获取是否开启全局变量开关
        is_open = InterfaceProjectBusiness.find_public_variable_type(project_id)
        with db.auto_commit():
            # 遍历用例
            temp_case = self.case_assemble(case_ids, case_type, is_open, case_env)
        return temp_case
    
    def handler_env_variable(self, case_env, variables_dict):
        # 根据env处理环境变量
        variables_dict_keys = []
        new_variables_dict = {}
        for name in variables_dict.keys():
            variable_data = extract_config_variable(name)
            if variable_data:
                if case_env == variable_data.group(2):
                    variable_name = variable_data.group(1)
                    if variables_dict.get(variable_name):
                        variables_dict_keys.append(name)
                    else:
                        new_variables_dict[variable_name] = variables_dict.get(name)
                        variables_dict_keys.append(name)
                else:
                    variables_dict_keys.append(name)
        # 删除不是当前环境的变量
        for key in variables_dict_keys:
            del variables_dict[key]
        # 更新variables_dict值
        variables_dict.update(new_variables_dict)
    
    def get_environment(self):
        case_env = self.env
        # 不存在指定env,查询服务的
        if not case_env:
            project_data = InterfaceProject.query.filter_by(id=self.project_ids,
                                                            status=InterfaceCase.ACTIVE).first()
            environment = project_data.environment_choice
            if environment == 'first':
                case_env = 'test'
            elif environment == 'second':
                case_env = 'dev'
            elif environment == 'third':
                case_env = 'online'
            elif environment == 'fourth':
                case_env = 'ys'
        return case_env
    
    def get_func_methods(self, case_ids):
        _cases_data = InterfaceCase.query.filter(InterfaceCase.id.in_(case_ids))
        _func_list = []
        func_methods = {}
        for func_name in _cases_data:
            func_json_value = json.loads(func_name.func_address)
            if func_json_value:
                for value in func_json_value:
                    if value not in _func_list:
                        _func_list.append(value)
                        import_path = 'func_list.{}'.format(value.replace('.py', ''))
                        func_list = importlib.import_module(import_path)
                        importlib.reload(func_list)
                        module_functions_dict = {name: item for name, item in
                                                 vars(func_list).items() if
                                                 isinstance(item, types.FunctionType)}
                        func_methods.update(module_functions_dict)
        return func_methods
    
    def set_global_variable(self, config_type, config_variable, func_method_dict):
        for config_item in config_variable:
            variable_data = extract_config_variable(config_item.get('key'))
            if variable_data:
                if config_type == variable_data.group(2) and func_method_dict:
                    try:
                        global_value = ''
                        ext_func = extract_functions_debug(config_item.get('value'))
                        func = parse_function(ext_func[0])
                        if func_method_dict.get(func['func_name']):
                            global_value = func_method_dict[func['func_name']](*func['args'])
                        config_item['value'] = global_value
                    except Exception as e:
                        current_app.logger.info('替换全局变量出错了', str(e))
                        config_item['value'] = ''
                    config_item['key'] = variable_data.group(1)
    
    def build_report(self, jump_res, case_ids):
        case_names = [
            InterfaceCase.query.filter_by(id=scene_id, status=InterfaceCase.ACTIVE).first().name
            for scene_id in case_ids]
        read_status = '待阅'
        current_app.logger.info("当前用例名称长度: {}".format(len(case_names)))
        if len(case_names) > 20:
            # 截取入库，字段太长了
            name = case_names[0:20]
        else:
            name = case_names
        self.new_report_id = InterfaceReportBusiness.report_create(','.join(name), read_status,
                                                                   self.project_ids)
        with open('{}{}.txt'.format(REPORT_ADDRESS, self.new_report_id), 'w') as f:
            f.write(jump_res)
        return self.new_report_id
    
    @staticmethod
    def run_case(test_cases):
        now_time = datetime.datetime.now()
        # 用例执行入口
        res = main_ate(test_cases)
        # 报告
        current_app.logger.info('报告ing')
        res['time']['duration'] = "%.2f" % res['time']['duration']
        steps = res['stat']['teststeps']
        res['stat']['testsRun'] = res['stat']['teststeps']['total']
        res['stat']['successes_1'] = steps['successes']
        res['stat']['failures_1'] = steps['failures']
        res['stat']['errors_1'] = steps['errors']
        res['stat']['successes'] = "{} ({}%)".format(steps['successes'],
                                                     int(steps['successes'] / steps['total'] * 100))
        res['stat']['failures'] = "{} ({}%)".format(steps['failures'],
                                                    int(steps['failures'] / steps['total'] * 100))
        res['stat']['errors'] = "{} ({}%)".format(steps['errors'],
                                                  int(steps['errors'] / steps['total'] * 100))
        res['stat']['successes_scene'] = 0
        res['stat']['failures_scene'] = 0
        for num_1, res_1 in enumerate(res['details']):
            if res_1['success']:
                res['stat']['successes_scene'] += 1
            else:
                res['stat']['failures_scene'] += 1
        res['stat']['all_scene'] = res['stat']['successes_scene'] + res['stat']['failures_scene']
        
        res['stat']['successes_scene1'] = "{} ({}%)".format(res['stat']['successes_scene'],
                                                            int(res['stat']['successes_scene'] /
                                                                res['stat'][
                                                                    'all_scene'] * 100))
        res['stat']['failures_scene1'] = "{} ({}%)".format(res['stat']['failures_scene'],
                                                           int(res['stat']['failures_scene'] /
                                                               res['stat'][
                                                                   'all_scene'] * 100))
        
        res['time']['start_at'] = now_time.strftime('%Y/%m/%d %H:%M:%S')
        del res['stat']['teststeps']
        del res['stat']['testcases']
        details = copy.deepcopy(res['details'])
        for item in details:
            records = item['records']
            for record in records:
                record['meta_data'] = record['meta_datas']['data'][0]
                record['meta_data']['validators'] = record['meta_datas']['validators']
                record['meta_data']['response'].update(record['meta_datas']['stat'])
                if record['meta_data']['request'].__contains__("body"):
                    request_body = record['meta_data']['request']['body']
                    if isinstance(request_body, Markup):
                        record['meta_data']['request']['body'] = request_body.unescape()
                del record['meta_datas']
                del record['meta_datas_expanded']
        res['details'] = details
        jump_resp = json.dumps(res, ensure_ascii=False, default=encode_object, cls=JSONEncoder)
        return jump_resp
    
    @classmethod
    def get_case_info_config(cls, _temp_config, case_ids, scene_config=None):
        """合并接口用例与用例信息的函数文件
        :param _temp_config: 接口用例的函数文件
        :param case_ids: 接口用例id
        :param scene_config: 接口用例的变量,保存校验不需要此字段
        :return:
        """
        case_ids = InterfaceCaseRelationBusiness.get_case_var(case_ids)
        case_variables = []
        all_func = []
        for case_id in case_ids:
            case_func = ConfigBusiness.parse_func(case_id)
            func = [func for func in case_func]
            all_func = all_func + func
            case_variable = ConfigBusiness.parse_variable(case_id)
            if case_variable:
                case_variables.append(case_variable)
        # 关键字的函数文件添加到文件中
        _temp_config = json.loads(_temp_config) if isinstance(_temp_config, str) else _temp_config
        base_func = list(set(_temp_config + all_func))
        
        if scene_config is None:
            return base_func
        base_case_var = [c for c_var in case_variables for c in c_var]
        # 接口用例优先级比用例信息优先级高，接口用例参数所以放在后面，同名参数后面会替换前面
        scene_config = base_case_var + scene_config
        return base_func, scene_config
    
    def case_assemble(self, case_ids, case_type, is_open, case_env):
        """组装用例
        :param case_ids:
        :param case_type:
        :param is_open:
        :param case_env:
        :return:
        """
        temp_case = []
        for case_id in case_ids:
            case_data = InterfaceCase.query.filter_by(id=case_id,
                                                      status=InterfaceCase.ACTIVE).first()
            case_times = case_data.times if case_data.times else 1
            for s in range(case_times):
                _temp_config = copy.deepcopy(self.pro_config_data)
                _temp_config['config']['name'] = case_data.name
                # 获取需要导入的函数
                _temp_config['config']['import_module_functions'] = ['func_list.{}'.format(
                    f.replace('.py', '')) for f in json.loads(case_data.func_address)]
                # 获取业务集合的配置数据
                scene_config = json.loads(case_data.variable) if case_data.variable else []
                # 获取接口用例的配置数据
                if case_type == 1:
                    _temp_config['config'][
                        'import_module_functions'], scene_config = self.get_case_info_config(
                        _temp_config['config']['import_module_functions'], case_id, scene_config)
                # 合并公用项目配置和业务集合配置-用例需要根据开关来
                _temp_config = merge_config(_temp_config, scene_config, is_open)
                steps = InterfaceCaseData.query.filter_by(case_id=case_id,
                                                          execute_status=0).all()
                base_steps = self.get_case_steps(case_type, case_id, steps, _temp_config)
                for num, _step in enumerate(base_steps, 1):
                    if _step.status == 'true':
                        _temp_config['teststeps']. \
                            append(self.assemble_step(None, _step,
                                                      _temp_config['config']['request']['headers'],
                                                      self.pro_base_url, True, num))
                # 处理variables值List转Dict
                variables_dict = {}
                config_variables = copy.deepcopy(_temp_config['config']['variables'])
                for item in config_variables:
                    for key in item.keys():
                        variables_dict[key] = item[key]
                self.handler_env_variable(case_env, variables_dict)
                _temp_config['config']['variables'] = variables_dict
                del _temp_config['config']['request']
                temp_case.append(_temp_config)
        return temp_case
    
    def get_case_steps(self, case_type, case_id, steps, _temp_config):
        """获取所有用例步骤
        :param case_type: 用例类型
        :param case_id: 接口用例id
        :param steps: 步骤
        :param _temp_config: 配置
        :return: 所有步骤
        """
        # 接口用例
        if case_type == 1:
            base_steps = InterfaceCaseRelationBusiness.handle_interface_case(case_id, steps,
                                                                             _temp_config)
        else:
            # 关键字
            base_steps = InterfaceCaseData.query.filter_by(case_id=case_id,
                                                           execute_status=InterfaceCaseData.ACTIVE). \
                order_by(InterfaceCaseData.num.asc()).all()
        return base_steps
